﻿using System;
using System.Collections.Generic;
using System.DirectoryServices;
using System.IO;
using System.Linq;
using System.Net;
using System.Xml;
using System.Xml.XPath;

namespace ApiScanner.Core
{
    internal sealed class TopologyScanner
    {
        private readonly DirectoryEntry _ConfigurationNamingContext;
        private readonly string[] _Servers;
        private IList<MailboxServer> _MailboxServers;
        private Stack<PublicFolder> _PublicFolderStack;
        private readonly DirectoryEntry _RootNamingContext;

        public TopologyScanner(string domain, string[] servers)
        {
            if (string.IsNullOrEmpty(domain)) throw new ArgumentNullException("domain");
            _Servers = servers;

            var root = new DirectoryEntry(string.Format("LDAP://{0}/RootDSE", domain));
            var configNamingContextString = root.Properties["configurationNamingContext"].Value as string;

            //DirectoryEntry gcRoot = new DirectoryEntry("GC://" + domain + "/RootDSE");
            //string rootDomainNamingContextString = gcRoot.Properties["rootDomainNamingContext"].Value as string;

            _RootNamingContext = new DirectoryEntry(string.Format("GC://{0}", domain));
            _ConfigurationNamingContext = new DirectoryEntry(string.Format("LDAP://{0}", configNamingContextString));
        }


        public IEnumerable<Mdb> Mdbs { get; private set; }

        public void ScanTopology()
        {
            _MailboxServers = EnumerateServers();
            Mdbs = EnumerateMdbs();
        }

        private IList<MailboxServer> EnumerateServers()
        {
            var mailboxServers = new List<MailboxServer>();
            DirectorySearcher searcher;
            if (_Servers == null)
            {
                searcher = new DirectorySearcher(_ConfigurationNamingContext,
                                                 "(&(objectclass=msexchexchangeserver)(name=*)(networkAddress=*)(serialNumber=*))");
            }
            else
            {
                var serverClause = _Servers.Aggregate(string.Empty,
                                                      (current, server) =>
                                                      string.Format("{0}(name={1})", current, server));

                if (_Servers.Length > 1)
                {
                    serverClause = string.Format("(|{0})", serverClause);
                }

                searcher = new DirectorySearcher(_ConfigurationNamingContext, string.Format("(&(objectclass=msexchexchangeserver)(networkAddress=*)(serialNumber=*){0})", serverClause));
            }
            using (searcher)
            {

                searcher.PropertiesToLoad.Add("name");
                searcher.PropertiesToLoad.Add("serialnumber");
                searcher.PropertiesToLoad.Add("distinguishedname");
                searcher.PropertiesToLoad.Add("networkaddress");

                using (var resultCollection = searcher.FindAll())
                {
                    foreach (SearchResult result in resultCollection)
                    {
                        if (!(result.Properties.Contains("name") &&
                              result.Properties.Contains("distinguishedname") &&
                              result.Properties.Contains("serialnumber") &&
                              result.Properties.Contains("networkaddress")))
                        {
                            // something went wrong with the AD query
                            throw new InvalidOperationException("AD query failed when attempting to enumerate servers.");
                        }

                        var networkAddress = "";
                        foreach (var valueString in
                            result.Properties["networkaddress"].Cast<string>().Where(
                                valueString => valueString.StartsWith("ncacn_ip_tcp:")))
                        {
                            networkAddress = valueString.Substring(13, valueString.Length - 13);
                        }

                        mailboxServers.Add(new MailboxServer(
                                               result.Properties["name"][0].ToString(),
                                               result.Properties["serialnumber"][0].ToString(),
                                               result.Properties["distinguishedname"][0].ToString(),
                                               networkAddress));
                    }
                }
            }

            return mailboxServers;
        }

        private IEnumerable<Mdb> EnumerateMdbs()
        {
            IList<Mdb> mdbs = new List<Mdb>();
            using (var searcher = new DirectorySearcher(_ConfigurationNamingContext, "(&(objectclass=msexchmdb)(msexchowningserver=*))"))
            {
                searcher.PropertiesToLoad.Add("distinguishedname");
                searcher.PropertiesToLoad.Add("msexchowningserver");
                searcher.PropertiesToLoad.Add("objectclass");
                searcher.PageSize = 1000;

                using (var resultCollection = searcher.FindAll())
                {
                    foreach (SearchResult result in resultCollection)
                    {
                        if (!(result.Properties.Contains("objectclass") &&
                              result.Properties.Contains("distinguishedname") &&
                              result.Properties.Contains("msexchowningserver")))
                        {
                            // something went wrong with the AD query
                            throw new Exception("AD query failed when attempting to enumerate mailbox databases.");
                        }

                        var type = (result.Properties["objectclass"].Contains("msExchPrivateMDB"))
                                       ?
                                           MdbType.Private
                                       : MdbType.Public;

                        var thisMdb = new Mdb(
                            result.Properties["distinguishedname"][0].ToString(),
                            result.Properties["msexchowningserver"][0].ToString(),
                            type);

                        foreach (var server in _MailboxServers)
                        {
                            if (server.DistinguishedName == thisMdb.OwningServer)
                                thisMdb.OwningServerBL = server;
                        }

                        if (thisMdb.OwningServerBL != null)
                            //Added this check so that we won't count MDBs not on the specified server
                        {
                            mdbs.Add(thisMdb);
                        }
                    }
                }
            }

            return mdbs;
        }

        public IEnumerable<Mailbox> EnumerateMailboxes(Mdb mdb)
        {
            using (var searcher = new DirectorySearcher(_RootNamingContext, "(homemdb=" + mdb.Name + ")"))
            {
                searcher.PropertiesToLoad.Add("mail");
                searcher.PropertiesToLoad.Add("proxyaddresses");

                searcher.PageSize = 1000;

                using (var resultCollection = searcher.FindAll())
                {
                    foreach (SearchResult result in resultCollection)
                    {
                        var smtpAddress = "";
                        var mailboxType = MailboxType.Unknown;

                        if (result.Properties.Contains("mail"))
                        {
                            mailboxType = MailboxType.RegularMailbox;
                            smtpAddress = result.Properties["mail"][0].ToString();
                        }

                        yield return new Mailbox(smtpAddress, mailboxType);
                    }
                }
            }
        }

        private void EnumerateChildFoldersIteratively(Uri pfRootPath)
        {
            const string searchBody = "<?xml version=\"1.0\"?>" +
                                      "<g:searchrequest xmlns:g=\"DAV:\" >" +
                                      "<g:sql>\r\n" +
                                      "SELECT \"DAV:href\", \"http://schemas.microsoft.com/exchange/content-href\" \r\n" +
                                      "FROM Scope('HIERARCHICAL TRAVERSAL OF \"\"')\r\n" +
                                      "</></>";

            var request = DavSearchHelper.CreateWebRequest(pfRootPath, searchBody);


            using (var response = request.GetResponse())
            {
                if (response.ContentType != "text/xml")
                {
                    throw new InvalidOperationException("The response returned from the server is not text/xml.");
                }
                
                var childFolders = ParsePublicFolderXmlStream(response.GetResponseStream());
                if (childFolders == null) return;
                foreach (var folder in childFolders)
                {
                    _PublicFolderStack.Push(folder);
                }
            }
        }

        public IEnumerable<Artifact> EnumerateAndScanPublicFolders(string serverHostName, bool useSsl, string domain, Action<string> progressHandler)
        {
            _PublicFolderStack = new Stack<PublicFolder>();
            Exception exception = null;
            try
            {
                EnumeratePublicRootFolders(serverHostName, useSsl, domain);
            }
            catch (WebException ex)
            {
                var response = (HttpWebResponse)ex.Response;
                if (response == null || response.StatusCode != HttpStatusCode.Forbidden || useSsl)
                {
                    exception = ex;
                }
                else 
                {
                    try
                    {
                        EnumeratePublicRootFolders(serverHostName, true, domain);
                    }
                    catch (Exception e)
                    {
                        exception = e;
                    }
                }
            }
            if (exception != null)
            {
                yield return new ScannerError(string.Format("Failed to enumerate public folder root on the server {0}", serverHostName), exception);
                yield break;
            }

            while (_PublicFolderStack.Count > 0)
            {
                var folder = _PublicFolderStack.Pop();
                exception = null;
                IEnumerable<StoreEvent> storeEvents = null;
                try
                {
                    progressHandler(string.Format("Scanning public folder {0}", folder.Path));
                    storeEvents = EventFinder.GetEvents(domain, folder.Path.ToString(), MailboxType.PublicFolder, useSsl);
                }
                catch (Exception ex)
                {
                    exception = ex;
                }
                if (exception != null)
                {
                    yield return new ScannerError(string.Format("Getting events from public folder {0}", folder.Path), exception);
                    continue;
                }
                if (storeEvents != null)
                {
                    foreach (var storeEvent in storeEvents)
                    {
                        yield return new StoreEventBinding(storeEvent, MdbType.Public, string.Empty);
                    }
                }
                try
                {
                    EnumerateChildFoldersIteratively(folder.Path);
                }
                catch (Exception ex)
                {
                    exception = ex;
                }
                if (exception != null)
                {
                    yield return
                        new ScannerError(string.Format("Failed to enumerate child folders of {0}", folder.Path),
                                         exception);
                }
            }
        }

        private void EnumeratePublicRootFolders(string serverHostName, bool useSsl, string domain)
        {
            EnumerateChildFoldersIteratively(DavSearchHelper.BuildPath(serverHostName, "", MailboxType.PublicFolder,
                                                                       useSsl, domain));
            EnumerateChildFoldersIteratively(DavSearchHelper.BuildPath(serverHostName, "",
                                                                       MailboxType.SystemPublicFolder, useSsl,
                                                                       domain));
        }

        private static IEnumerable<PublicFolder> ParsePublicFolderXmlStream(Stream responseStream)
        {
            IList<PublicFolder> publicFolders = new List<PublicFolder>();

            const string davNamespace = "DAV:";
            const string proptagNamespace = "http://schemas.microsoft.com/mapi/proptag/";
            const string exchangeNamespace = "http://schemas.microsoft.com/exchange/";

            var reader = XmlReader.Create(responseStream);

            var nsmgr = new XmlNamespaceManager(reader.NameTable);
            nsmgr.AddNamespace("dav", davNamespace);
            nsmgr.AddNamespace("e", proptagNamespace);
            nsmgr.AddNamespace("ex", exchangeNamespace);

            var doc = new XPathDocument(reader);

            foreach (XPathNavigator element in doc.CreateNavigator().Select(
                "//dav:response[dav:propstat/dav:status = 'HTTP/1.1 200 OK']",
                nsmgr))
            {
                var thisFolder = new PublicFolder
                                     {
                                         Path = new Uri(element.SelectSingleNode("dav:href", nsmgr).Value)
                                     };

                if (element.SelectSingleNode("dav:propstat/dav:prop/ex:content-href", nsmgr) != null)
                {
                    thisFolder.Path =
                        new Uri(element.SelectSingleNode("dav:propstat/dav:prop/ex:content-href", nsmgr).Value);
                }

                publicFolders.Add(thisFolder);
            }

            return publicFolders;
        }


    }
}